﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.UI.WebControls;

namespace Inovout.Web.Mvc
{
    internal static class TemplateHelpers
    {
        private static readonly Dictionary<DataBoundControlMode, string> _modeViewPaths =
            new Dictionary<DataBoundControlMode, string>
            {
                { DataBoundControlMode.ReadOnly, "DisplayTemplates" },
                { DataBoundControlMode.Edit, "EditorTemplates" }
            };

        private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultDisplayActions =
            new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase)
            {
                { "EmailAddress", DisplayTemplates.EmailAddressTemplate },
                { "HiddenInput", DisplayTemplates.HiddenInputTemplate },
                { "Html", DisplayTemplates.HtmlTemplate },
                { "Text", DisplayTemplates.StringTemplate },
                { "Url", DisplayTemplates.UrlTemplate },
                { "Collection", DisplayTemplates.CollectionTemplate },
                { typeof(bool).Name, DisplayTemplates.BooleanTemplate },
                { typeof(decimal).Name, DisplayTemplates.DecimalTemplate },
                { typeof(string).Name, DisplayTemplates.StringTemplate },
                { typeof(object).Name, DisplayTemplates.ObjectTemplate },
            };

        private static readonly Dictionary<string, Func<HtmlHelper, string>> _defaultEditorActions =
            new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase)
            {
                { "HiddenInput", EditorTemplates.HiddenInputTemplate },
                { "MultilineText", EditorTemplates.MultilineTextTemplate },
                { "Password", EditorTemplates.PasswordTemplate },
                { "Text", EditorTemplates.StringTemplate },
                { "Collection", EditorTemplates.CollectionTemplate },
                { "PhoneNumber", EditorTemplates.PhoneNumberInputTemplate },
                { "Url", EditorTemplates.UrlInputTemplate },
                { "EmailAddress", EditorTemplates.EmailAddressInputTemplate },
                { "DateTime", EditorTemplates.DateTimeInputTemplate },
                { "Date", EditorTemplates.DateInputTemplate },
                { "Time", EditorTemplates.TimeInputTemplate },
                { typeof(Color).Name, EditorTemplates.ColorInputTemplate },
                { typeof(byte).Name, EditorTemplates.NumberInputTemplate },
                { typeof(sbyte).Name, EditorTemplates.NumberInputTemplate },
                { typeof(int).Name, EditorTemplates.NumberInputTemplate },
                { typeof(uint).Name, EditorTemplates.NumberInputTemplate },
                { typeof(long).Name, EditorTemplates.NumberInputTemplate },
                { typeof(ulong).Name, EditorTemplates.NumberInputTemplate },
                { typeof(bool).Name, EditorTemplates.BooleanTemplate },
                { typeof(decimal).Name, EditorTemplates.DecimalTemplate },
                { typeof(string).Name, EditorTemplates.StringTemplate },
                { typeof(object).Name, EditorTemplates.ObjectTemplate },
            };

        internal static string CacheItemId = Guid.NewGuid().ToString();

        internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName,
                                                         DataBoundControlMode mode, GetViewNamesDelegate getViewNames,
                                                         GetDefaultActionsDelegate getDefaultActions, int? captionWidth, int? partWidth);

        internal delegate Dictionary<string, Func<HtmlHelper, string>> GetDefaultActionsDelegate(DataBoundControlMode mode);

        internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints);

        internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName,
                                                        string templateName, DataBoundControlMode mode, object additionalViewData, int? captionWidth, int? partWidth);

        internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions, int? captionWidth, int? partWidth)
        {
            viewData[CaptionWidthKey] = captionWidth;
            viewData[PartWidthKey] = partWidth;

            Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html);
            Dictionary<string, Func<HtmlHelper, string>> defaultActions = getDefaultActions(mode);
            string modeViewPath = _modeViewPaths[mode];

            foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName))
            {
                string fullViewName = modeViewPath + "/" + viewName;
                ActionCacheItem cacheItem;

                if (actionCache.TryGetValue(fullViewName, out cacheItem))
                {
                    if (cacheItem != null)
                    {
                        return cacheItem.Execute(html, viewData);
                    }
                }
                else
                {
                    ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
                    if (viewEngineResult.View != null)
                    {
                        actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };

                        using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
                        {
                            viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
                            return writer.ToString();
                        }
                    }

                    Func<HtmlHelper, string> defaultAction;
                    if (defaultActions.TryGetValue(viewName, out defaultAction))
                    {
                        actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
                        return defaultAction(MakeHtmlHelper(html, viewData));
                    }

                    actionCache[fullViewName] = null;
                }
            }

            throw new InvalidOperationException("TemplateHelpers_NoTemplate"
                //    String.Format(
                //        CultureInfo.CurrentCulture,
                //        MvcResources.TemplateHelpers_NoTemplate,
                //        viewData.ModelMetadata.RealModelType.FullName)
            );
        }
        internal static Type GetRealModelType(ModelMetadata modelMetadata)
        {
            if (modelMetadata.Model != null && !TypeHelpers.IsNullableValueType(modelMetadata.ModelType))
            {
                return modelMetadata.Model.GetType();
            }
            else
            {
                return modelMetadata.ModelType;
            }
        }
        internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html)
        {
            HttpContextBase context = html.ViewContext.HttpContext;
            Dictionary<string, ActionCacheItem> result;

            if (!context.Items.Contains(CacheItemId))
            {
                result = new Dictionary<string, ActionCacheItem>();
                context.Items[CacheItemId] = result;
            }
            else
            {
                result = (Dictionary<string, ActionCacheItem>)context.Items[CacheItemId];
            }

            return result;
        }

        internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode)
        {
            return mode == DataBoundControlMode.ReadOnly ? _defaultDisplayActions : _defaultEditorActions;
        }

        internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints)
        {
            foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s)))
            {
                yield return templateHint;
            }

            // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
            Type fieldType = Nullable.GetUnderlyingType(GetRealModelType(metadata)) ?? GetRealModelType(metadata);

            // TODO: Make better string names for generic types
            yield return fieldType.Name;

            if (!metadata.IsComplexType)
            {
                yield return "String";
            }
            else if (fieldType.IsInterface)
            {
                if (typeof(IEnumerable).IsAssignableFrom(fieldType))
                {
                    yield return "Collection";
                }

                yield return "Object";
            }
            else
            {
                bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);

                while (true)
                {
                    fieldType = fieldType.BaseType;
                    if (fieldType == null)
                    {
                        break;
                    }

                    if (isEnumerable && fieldType == typeof(Object))
                    {
                        yield return "Collection";
                    }

                    yield return fieldType.Name;
                }
            }
        }

        internal static MvcHtmlString Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData, int? captionWidth, int? partWidth)
        {
            return MvcHtmlString.Create(Template(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper, captionWidth, partWidth));
        }

        // Unit testing version
        internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName,
                                        DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper, int? captionWidth, int? partWidth)
        {
            return templateHelper(html,
                                  ModelMetadata.FromStringExpression(expression, html.ViewData),
                                  htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
                                  templateName,
                                  mode,
                                  additionalViewData,
                                  captionWidth, partWidth);
        }

        internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
                                                                      string templateName, string htmlFieldName, DataBoundControlMode mode,
                                                                      object additionalViewData, int? captionwidth, int? partWidth)
        {
            return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper, captionwidth, partWidth));
        }

        // Unit testing version
        internal static string TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
                                                               string templateName, string htmlFieldName, DataBoundControlMode mode,
                                                               object additionalViewData, TemplateHelperDelegate templateHelper, int? captionWidth, int? partWidth)
        {
            return templateHelper(html,
                                  ModelMetadata.FromLambdaExpression(expression, html.ViewData),
                                  htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
                                  templateName,
                                  mode,
                                  additionalViewData,
                                  captionWidth, partWidth);
        }

        internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, int? captionWidth, int? partWidth)
        {
            return TemplateHelper(html, metadata, htmlFieldName, templateName, mode, additionalViewData, ExecuteTemplate, captionWidth, partWidth);
        }
        internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate, int? captionWidth, int? partWidth)
        {
            // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because
            // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically
            // decorates it with a [ReadOnly] attribute...

            if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model))
            {
                metadata.Model = null;
            }

            object formattedModelValue = metadata.Model;
            if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly)
            {
                formattedModelValue = metadata.NullDisplayText;
            }

            string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString;
            if (metadata.Model != null && !String.IsNullOrEmpty(formatString))
            {
                formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model);
            }

            // Normally this shouldn't happen, unless someone writes their own custom Object templates which
            // don't check to make sure that the object hasn't already been displayed
            object visitedObjectsKey = metadata.Model ?? GetRealModelType(metadata);
            if (GetVisitedObjects(html.ViewDataContainer.ViewData).Contains(visitedObjectsKey))
            {
                // DDB #224750
                return String.Empty;
            }

            ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData)
            {
                Model = metadata.Model,
                ModelMetadata = metadata,
                TemplateInfo = new TemplateInfo
                {
                    FormattedModelValue = formattedModelValue,
                    HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName),
                    //VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750
                }
            };
            viewData[VisitedObjectsKey] = new HashSet<object>(GetVisitedObjects(html.ViewContext.ViewData));


            if (additionalViewData != null)
            {
                foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData))
                {
                    viewData[kvp.Key] = kvp.Value;
                }
            }

            GetVisitedObjects(viewData).Add(visitedObjectsKey); // DDB #224750
            //viewData[CaptionWidthKey] = captionWidth;
            //viewData[PartWidthKey] = partWidth;
            return executeTemplate(html, viewData, templateName, mode, GetViewNames
                , GetDefaultActions, captionWidth, partWidth);
        }
        internal const string CaptionWidthKey = "_CaptionWidthKey";
        internal const string PartWidthKey = "_PartWidthKey";
        private const string VisitedObjectsKey = "_VisitedObjects";
        internal static HashSet<object> GetVisitedObjects(ViewDataDictionary viewData)
        {
            object result;
            if (viewData.TryGetValue(VisitedObjectsKey, out result))
            {
                return result as HashSet<object>;
            }
            else
            {
                result = new HashSet<object>();
                viewData[VisitedObjectsKey] = result;
                return result as HashSet<object>;
            }
        }
        // Helpers

        private static HtmlHelper MakeHtmlHelper(HtmlHelper html, ViewDataDictionary viewData)
        {
            return new HtmlHelper(
                new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer),
                new ViewDataContainer(viewData));
        }

        internal class ActionCacheCodeItem : ActionCacheItem
        {
            public Func<HtmlHelper, string> Action { get; set; }

            public override string Execute(HtmlHelper html, ViewDataDictionary viewData)
            {
                return Action(MakeHtmlHelper(html, viewData));
            }
        }

        internal abstract class ActionCacheItem
        {
            public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData);
        }

        internal class ActionCacheViewItem : ActionCacheItem
        {
            public string ViewName { get; set; }

            public override string Execute(HtmlHelper html, ViewDataDictionary viewData)
            {
                ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, ViewName);
                using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
                {
                    viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
                    return writer.ToString();
                }
            }
        }

        private class ViewDataContainer : IViewDataContainer
        {
            public ViewDataContainer(ViewDataDictionary viewData)
            {
                ViewData = viewData;
            }

            public ViewDataDictionary ViewData { get; set; }
        }
    }
}
